/**
 * 当前会话类
 * 
 * 几乎所有的请求都需要关联一个会话上下文，最直观的理解是：请求可以通过传相同的“token”字段将请求归到一个会话中
 * 
 * + 初始化会话实例
 * + 获取会话标识（令牌） token
 * + 获取当前小程序信息
 * + 获取会话用户
 * + 获取关联的卖酒服务器
 * + 获取历史页面栈（暂不实现）
 */
import store from '../../store/index'
import SessionSellingServerUtil, { SessionSellingServer } from './sessionSellingServer'
import SessionUserUtil, { SessionUser } from './sessionUser'
import HttpClient, { HttpResponse, NotLoginError, SessionExpiredError, JxbResponseError, NetworkError, JxbResponse, CommonBizLogicError } from '../../utils/network';
import PagePromoterUtil from '../common/pagePromoter';
import SellingServerAdapter from "../../dataAdapter/sellingServerAdapter"
import SessionUserAdapter from "../../dataAdapter/session/sessionUserAdapter"
import { RtLogUtil } from '../../utils/log';
import PageUtil from '../../shell/router/page';
/**
 * 小程序信息
 */
class Applet {
    /**
     * @property {object} 源数据
     */
    data

    /**
     * @returns {string} 小程序的APPID
     */
    get appId() {
        return this._data.appId + ''
    }

    /**
     * @returns {string} 小程序名
     */
    get name() {
        return this._data.name + ''
    }

    /**
     * @returns {string} 小程序logo地址
     */
    get logoUri() {
        return this._data.logoUri + ''
    }

    /**
     * @returns {string} 小程序码图片地址
     */
    get appletCodeUri() {
        return this._data.appletCodeUri + ''
    }

    /**
     * @param {object} initData 实例初始化时需要的数据
     */
    constructor(initData = {}) {
        if (typeof initData === 'object' && Object.keys(initData).length > 0) {
            this._data = initData;
        }
    }
}
/**
 * 会话信息
 */
export class Session {
    /**
     * @property {object} 源数据
     */
    data

    /**
     * @returns {string} 会话令牌/会话id
     */
    get token() {
        return this._data.token
    }

    /**
     * @returns {string} 微信用户openID
     */
    get openId() {
        return this._data.openId
    }

    /**
     * 获取当前会话的用户实例
     * 
     * @returns {Promise<SessionUser>}
     */
    getTheSessionUser = () => SessionUserUtil.getInstance()

    /**
     * 获取当前会话的用户实例（同步）
     * 
     * **Bug&Tips**
     * 
     * + 调用前请确保本地有相关的缓存数据，否则请对返回结果进行判断
     * + 获取失败将返回null
     * @returns {SessionUser|null}
     */
    getTheSessionUserSync = () => SessionUserUtil.getInstanceSync()
    /**
     * 当前访问的卖酒服务器
     * 
     * @returns {Promise<SessionSellingServer>}
     */
    getTheSellingServer = () => SessionSellingServerUtil.getInstance()
    /**
     * 获取当前访问的卖酒服务器（同步）
     * 
     * **Bug&Tips**
     * 
     * + 调用前请确保本地有相关的缓存数据，否则请对返回结果进行判断
     * + 获取失败将返回null
     * @returns {SessionSellingServer|null}
     */
    getTheSellingServerSync = () => SessionSellingServerUtil.getInstanceSync()
    /**
     * 设置/更新当前卖酒服务器
     * 
     * @todo 卖酒服务器变化后会触发卖酒服务器变化的事件（sellingServerChanged），同时进行卖酒服务器信息的刷新等操作
     * @todo 暂不实现
     * @param {SessionSellingServer} sellingServer 卖酒服务器
     */
    set theSellingServer(sellingServer) {

    }

    /**
     * @return {SessionSellingServer|null}  推广来源的卖酒服务器
     * @todo 暂不实现
     */
    getTheRecommendedServer() {

    }

    /**
     * 设置推广来源服务器
     * 
     * 每次通过分享进来时都设置该值
     * 
     * @todo 该参数变化时，触发 recommendedServerChanged 事件，是否切换卖酒服务器以及切花的界面展示形式交由该事件处理
     * @todo 暂不实现
     * @param {SessionSellingServer|null} sellingServer 推广来源
     */
    set theRecommendedServer(sellingServer) {

    }

    /**
     * 获取会话中的小程序信息
     * 
     * @returns {Promise<Applet>} 
     */
    getTheApplet() {
        return new Promise(resolve => {
            resolve(new Applet(this._data.appletData))
        })
    }

    /**
     * 获取会话中的小程序信息（同步）
     * 
     * **Bug&Tips**
     * 
     * + 调用前请确保本地有相关的缓存数据，否则请对返回结果进行判断
     * + 获取失败将返回null
     * @returns {Applet|null}
     */
    getTheAppletSync() {
        return new Applet(this._data.appletData)
    }

    /**
     * @param {object} initData 实例初始化时需要的数据
     */
    constructor(initData = {}) {
        if (typeof initData === 'object' && Object.keys(initData).length > 0) {
            this._data = initData
        }
    }
}
/**
 * 会话工具类
 */
export default class SessionUtil {
    /**
     * 从服务器端请求会话信息
     * 
     * wx.login
     * @returns {Promise<object>}
     */
    static requestServer() {
        return (new Promise((resolve, reject) => {
            // 获取wx登录code
            wx.login({
                success: function (res) {
                    if (res.code) {
                        resolve(res.code)
                    } else {
                        reject({
                            result: 0,
                            message: res.errMsg
                        })
                    }
                }
            })
        })).then(code => {
            let promoter = PagePromoterUtil.getLastPromoter(),
                requestData = { code: code, }
            // todo 来源卖酒服务器信息在这里设置
            if (typeof promoter !== 'undefined') {
                requestData.salesman_id = promoter.salesmanId
                requestData.consultant_id = promoter.salesmanVCardId
            }
            let requestProm = (new JxbHttpClient()).setNoToken(true).request({
                url: '/AppMaster/AppletSession/doInit',
                data: requestData,
                header: {
                    'content-type': 'json'
                }
            })
            console.debug('执行刷新会话的网络请求构造完成，即将发起请求', requestProm)
            return requestProm
        }).then((jxbResp) => {
            console.debug('完成刷新会话的网络请求，开始处理请求结果', jxbResp)
            if (jxbResp.isOk()) {
                return Promise.resolve(jxbResp)
            } else {
                console.debug(jxbResp.errorCode)
                return Promise.reject(new CommonBizLogicError(jxbResp.message, jxbResp.errorCode, jxbResp))
            }
        }).then(jxbResp => {
            let respData = jxbResp.data, sellingServerResp = respData.sellingServer, sessionUserResp = respData.sessionUser

            // 处理SellingServer模块相关数据
            let activeServerData = sellingServerResp.liquorSellingServer;
            let lastServerData = sellingServerResp.lastAccessedServer,
                recommendServerData = sellingServerResp.recommendedServer;
            let sellingServerData = SellingServerAdapter.convert(activeServerData)
            if (typeof lastServerData === 'object') {
                sellingServerData.lastServerData = SellingServerAdapter.convert(lastServerData)
            }
            if (typeof recommendServerData === 'object') {
                sellingServerData.recommendServerData = SellingServerAdapter.convert(recommendServerData)
            }
            // 处理SessionUser模块
            let sessionUserData = SessionUserAdapter.convert(sessionUserResp)
            return {
                sessionData: {
                    token: respData.token,
                    openId: respData.openId,
                    appletData: {
                        appId: respData.applet.appId,
                        name: respData.applet.name,
                        logoUri: respData.applet.logoUri,
                        appletCodeUri: respData.applet.appletCodeUri
                    },
                },
                sellingServerData: sellingServerData,
                sessionUserData: sessionUserData
            }
        })
    }

    /**
     * 从服务器直接获取当前会话用户信息
     * 
     * @returns {Promise<[Session,SessionSellingServer,SessionUser]>|Promise<Session>}
     */
    static getInstanceFromServer(multiReturn = false) {
        return this.requestServer()
            .then(data => {
                if (multiReturn) {
                    return [new Session(data.sessionData), new SessionSellingServer(data.sellingServerData), new SessionUser(data.sessionUserData)]
                }
                return new Session(data)
            })
    }

    /**
     * 获取会话实例
     * 
     * 默认从缓存中获取实例，如果失败则从服务器获取
     * @returns {Promise<Session>}
     */
    static getInstance() {
        let prom, session = store.global('Session')
        if (session instanceof Session && Object.keys(session._data).length > 0) {
            // console.log('内存中已有会话实例', session)
            prom = new Promise(resolve => {
                resolve(session)
            })
        } else {
            let cacheData = this.requestCacheSync()
            if (Object.keys(cacheData).length === 0) {
                // console.log('从服务器获取会话示例，并刷新本地缓存')
                prom = this.renewCache()
            } else {
                // console.log('本地存储中有会话数据，直接恢复', cacheData)
                session = new Session(cacheData)
                store.global('Session', session)
                prom = new Promise((resolve) => {
                    resolve(session)
                })
            }
        }
        return prom
    }

    /**
     * 获取实例（同步）
     * 
     * 
     * 立即返回会话实例
     * 
     * **Bug&Tips**
     * 
     * + 由于该方法只从内存和本地缓存中获取会话信息，不会向服务器发起请求，因此该方法依赖于“会话初始化流程”的执行结果。
     * + 使用该方法时，请确保全局变量中或本地缓存中已经存在相关数据，或者在获取后对结果进行判断
     * + 当本地没有缓存信息时调用了本方法，将会返回null
     * @returns {Session|null}
     */
    static getInstanceSync() {
        let session = store.global('Session')
        if (session instanceof Session && Object.keys(session._data).length > 0) {
            // console.log('内存中已有会话实例', session)
            // do nothing
        } else {
            let cacheData = this.requestCacheSync()
            if (Object.keys(cacheData).length > 0) {
                // console.log('本地存储中有会话数据，直接恢复并存到全局数据中', cacheData)
                session = new Session(cacheData)
                store.global('Session', session)
            } else {
                // console.log('本地存储中没有会话数据，返回null')
                session = null
            }
        }
        return session
    }

    /**
     * 获取缓存数据
     * 
     * @returns {object}
     */
    static requestCacheSync() {
        let cacheData = wx.getStorageSync('Session')
        if (typeof cacheData !== 'object') {
            cacheData = {}
        }
        return cacheData
    }
    /**
     * 更新本地缓存
     * 
     * 从服务器同步用户信息到本地缓存
     * 
     * @returns {Promise<Session>} 返回最新的缓存数据
     */
    static renewCache() {
        let promCacheName = 'session.renewCacheProm',
            instanceDataProm = store.global(promCacheName)
        if (instanceDataProm instanceof Promise) {
            // do nothing 复用已存在的Promise实例
            console.debug('并发检测，存在并发的刷新session的请求，复用已向全局声明的请求', instanceDataProm)
        } else {
            console.debug('并发检测，不存在并发的刷新session的请求')
            instanceDataProm = this.getInstanceFromServer(true)
                .then(instances => {
                    console.log(instance)
                    let [instance, sellingServer, sessionUser] = instances
                    console.debug('从远程获取到session数据，更新本地缓存并存到全局数据中')
                    store.global('Session', instance)
                    wx.setStorageSync('Session', instance._data)
                    // 兼容原缓存方式
                    wx.setStorageSync('getInstance', {
                        applet_appid: instance._data.appletData.appId,
                        applet_name: instance._data.appletData.name,
                        applet_logo: instance._data.appletData.logoUri,
                        applet_identifier: instance._data.appletData.appletCodeUri,
                    });
                    wx.setStorageSync('zhuname', instance._data.appletData.name);
                    wx.setStorageSync('openid', instance._data.openId);
                    wx.setStorageSync('token', instance._data.token);
                    store.global(promCacheName, null) // 向全局声明请求已结束
                    return Promise.all([instance, SessionSellingServerUtil.renewCache(sellingServer), SessionUserUtil.renewCache(sessionUser)])
                }).then(instances => {
                    return instances[0]
                }).catch(error => {
                    store.global(promCacheName, null) // 向全局声明请求已结束
                    throw error
                })
            store.global(promCacheName, instanceDataProm) // 向全局声明请求已存在
        }
        return instanceDataProm
    }
}

// HttpClient

/**
 * 会话http客户端类
 * 
 * **用法**
 * 
 * ```js
 * (new JxbHttpClient()).request({
    *      url: '/AppMaster/Index/index',
    *      data: {},
    *      method: 'GET'
    *      header: {
    *          'Content-Type': 'json'
    *      }
    * }).then(jxbResp => {
    *      console.log('get response', jxbResp) // 同 success 的回调逻辑
    * }).catch(err => {
    *      console.log('client or network error', err) // 同 fail 的回调逻辑, 一般只需要处理 CommonBizLogicError
    * })
    * 
    * // 简单的get请求
    * (new JxbHttpClient()).request('/AppMaster/Index/index')
    * .then(jxbResp => {
    *      console.log('get response', jxbResp) // 同 success 的回调逻辑
    * }).catch(err => {
    *      console.log('client or network error', err) // 同 fail 的回调逻辑, 一般只需要处理 CommonBizLogicError
    * })
    * ```
    */
export class JxbHttpClient extends HttpClient {
    get tryCount() {
        if (typeof this._data.tryCount === 'undefined') {
            this._data.tryCount = 2
        }
        return this._data.tryCount > 0 ? this._data.tryCount : 0
    }
    /**
     * @param {number} tryCount 失败后的重试次数
     * @returns {this}
     */
    setFailTryCount(tryCount) {
        this._data.tryCount = tryCount
        return this
    }

    /**
     * @returns {boolean} 请求是否不携带会话令牌 true = 不携带token， false = 携带token
     */
    get noToken() {
        return !!this._data.noToken
    }

    /**
     * 设置请求是否不携带会话令牌
     * 
     * 不调用此参数，默认需要携带token
     * @param {boolean} [noToken = true]  true = 不携带token， false = 携带token
     */
    setNoToken(noToken = true) {
        if (noToken !== false) noToken = true
        this._data.noToken = noToken
        return this
    }
    /**
     * 获取客户端实例
     * 
     * new JxbHttpClient() 的别名
     */
    static getClient(){
        return new this()
    }
    constructor() {
        super()
    }

    /**
     * 执行请求操作
     * 
     * @param {object|string} config 请求参数,除没有回调函数的参数外，格式与wx.request一致;此外也支持只传url
     * @param {string} config.url 支持带域名和不带域名两种形式
     * @param {string} config.data
     * @param {string} config.method
     * @param {string} config.header
     * @param {number} config.interceptBizEvent 拦截事件，默认为JxbHttpInterceptor.EVENT_ALL；如果不处理“需要登陆”的返回结果，可以赋值为 ~JxbHttpInterceptor.EVENT_ALL&JxbHttpInterceptor.EVENT_NOT_LOGIN
     * @returns {Promise<JxbResponse>} 成功时返回服务器响应结果JxbResponse，
     * 基础网络错误返回NetworkError, 服务器响应状态码错误返回JxbResponseError，接口返回的结果为100时返回CommonBizLogicError，不需要再考虑其他错误
     */
    request(config) {
        if (typeof config === 'string') {
            config = { url: config }
        }
        let successCall,failCall
        if (typeof config === 'object') {
            if(typeof config.success === 'function'){
                successCall = config.success
                console.warn('不建议使用success参数处理成功回调，请在then方法中完成相关的逻辑')
            }
            if(typeof config.fail === 'function') {
                failCall = config.fail
                console.warn('不建议使用fail参数处理失败回调，请在catch方法中完成相关的逻辑')
            }
            config.success = config.fail = undefined
            this.setConfig(config)
        }
        let prom
        if (!this.noToken) {
            // 使用会话令牌的请求，先获取令牌
            prom = SessionUtil.getInstance().then(session => {
                // 往header中添加token字段，为服务端接口升级做准备
                this.setBody('token', session.token).setHeader('token', session.token)
            })
        } else {
            // 不使用令牌，do nothing
            prom = Promise.resolve()
        }
        return prom.then(() => {
            return new Promise((resolve, reject) => {
                this.setSuccessCall((res) => {
                    this.response = new HttpResponse(res)
                    resolve(this.response)
                }).setFailCall((err) => {
                    console.debug('网络请求错误', err, config) // 可能是客户端网络问题或无法与服务器通信 
                    // todo 对于客户端网络问题，可以考虑进一步优化客户端网络出错的情况
                    this.err = err
                    reject(new NetworkError(err.errMsg, err))
                })
                this._data.requestTask = super.request(this.config)
            })
        }).then(httpResp => {
            if (!httpResp.isSucceeded()) {
                console.debug('服务器端响应状态码错误', httpResp)
                return Promise.reject(new JxbResponseError('response code:' + httpResp.code, httpResp))
            }
            let jxbResp = new JxbResponse(httpResp.body)
            if(typeof successCall === 'function'){
                successCall(httpResp.rawResp)
            } else if (jxbResp.errorCode === 403) {
                console.debug('会话已过期或令牌无效', jxbResp)
                return Promise.reject(new SessionExpiredError(jxbResp.message, jxbResp.errorCode, jxbResp, httpResp))
            } else if (jxbResp.errorCode === 409) {
                console.debug('会话用户权限不足，需要登录账户', jxbResp)
                return Promise.reject(new NotLoginError(jxbResp.message, jxbResp.errorCode, jxbResp, httpResp))
            } else if (jxbResp.errorCode === 100) {
                return Promise.reject(new CommonBizLogicError(jxbResp.message, jxbResp.errorCode, jxbResp, httpResp))
            }
            return jxbResp
        }).catch(err => {
            if (err instanceof NetworkError){
                RtLogUtil.error('客户端发起网络请求错误', err.message, this._data, err.failResponse)
                if(typeof failCall === 'function') failCall(err.failResponse)
                throw err;
            } else if (err instanceof SessionExpiredError) {
                if(!(this.interceptBizEvent & JxbHttpInterceptor.EVENT_TOKEN_EXPIRED)){
                    console.debug('不对会话过期事件进行拦截')
                    throw err
                }
                // 刷新token
                console.debug('开始执行会话初始化操作')
                if (typeof this._data.sessionRetryCount === 'undefined') {
                    this._data.sessionRetryCount = 2
                }
                if (this._data.sessionRetryCount > 0) {
                    this._data.sessionRetryCount--
                    return SessionUtil.renewCache().then((session) => {
                        console.debug('会话初始化操作完成,再次执行请求操作', session, this)
                        return this.setFailTryCount(this.tryCount).request()
                    })
                } else {
                    console.debug('会话初始化失败次数过多, 不再重试', this)
                    RtLogUtil.error('会话初始化失败次数过多, 不再重试', err.message, this._data, err.httpResponse)
                    throw err
                }
            } else if (err instanceof NotLoginError) {
                if(!(this.interceptBizEvent & JxbHttpInterceptor.EVENT_NOT_LOGIN)){
                    console.debug('不对未登录事件进行拦截')
                    throw err
                }
                console.debug('NotLoginError', err)
                let curPage = PageUtil.getHistory(-1),
                    accessUri = PageUtil.getUri(curPage)
                accessUri = encodeURIComponent(accessUri);
                console.log('referer', curPage, accessUri);
                wx.navigateTo({
                    url: `/pages/my/logintype/logintype?from=4&_r=${accessUri}`
                })
                return Promise.reject(0);
            } else if (err instanceof JxbResponseError) {
                console.debug('JxbResponseError', err)
                // 只对服务器5**时重发请求
                if (Math.floor(err.httpResponse.code / 100) === 5) {
                    if (this.tryCount > 0) {
                        console.debug(`剩余请求次数${this.tryCount}, 重新发起请求`, this)
                        return this.setFailTryCount(this.tryCount - 1).request()
                    } else {
                        console.debug('请求重试次数用完, 抛错', this)
                        RtLogUtil.error('服务端响应时出误，尝试次数耗尽', err.message, this._data, err.httpResponse)
                        throw err
                    }
                }
            } else {
                console.debug('普用业务逻辑错误', this)
                throw err
            }
        })
    }

    /**
     * 执行GET请求操作
     *
     * @param {object|string} config 请求参数,除没有回调函数的参数外，格式与wx.request一致;此外也支持只传url
     * @param {string} config.url 支持带域名和不带域名两种形式
     * @param {string} config.data
     * @param {string} config.header
     * @returns {Promise<JxbResponse>} 成功时返回服务器响应结果JxbResponse，
     * 基础网络错误返回NetworkError, 服务器响应状态码错误返回JxbResponseError，接口返回的结果为100时返回CommonBizLogicError，不需要再考虑其他错误
     */
    get(config) {
        if (typeof config === 'object') {
            config.method='GET'
        }else{
            this.setMethod('GET')
        }
        return this.request(config)
    }
    /**
      * 执行POST请求操作
      * 
      * @param {object|string} config 请求参数,除没有回调函数的参数外，格式与wx.request一致;此外也支持只传url
      * @param {string} config.url 支持带域名和不带域名两种形式
      * @param {string} config.data
      * @param {string} config.header
      * @returns {Promise<JxbResponse>} 成功时返回服务器响应结果JxbResponse，
      * 基础网络错误返回NetworkError, 服务器响应状态码错误返回JxbResponseError，接口返回的结果为100时返回CommonBizLogicError，不需要再考虑其他错误
      */
    post(config) {
        if (typeof config === 'object') {
            config.method='POST'
        }else{
            this.setMethod('POST')
        }
        return this.request(config)
    }
}

/**
 * 请求拦截器
 */
export class JxbHttpInterceptor {
    /**
     * 拦截事件：通用错误
     * 
     * @readonly
     */
    static get EVENT_NORMAL_ERROR() {
        return 1;
    }

    /**
     * 拦截事件：令牌过期
     * 
     * @readonly
     */
    static get EVENT_TOKEN_EXPIRED() {
        return 2;
    }

    /**
     * 拦截事件：未登录
     * 
     * @readonly
     */
    static get EVENT_NOT_LOGIN() {
        return 4;
    }

    /**
     * 拦截事件：全部
     * 
     * @readonly
     */
    static get EVENT_ALL() {
        return 7;
    }  
}